#
# peerscape.core.path
#
# Copyright 2007-2008 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Ken Rimey <rimey@hiit.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

"""
Encoding of dataset keys as slash-separated paths

Outside of peerscape.core, we generally prefer to represent dataset
keys as Unicode slash-separated paths instead of as lists of byte
strings.
"""

from binascii import hexlify, unhexlify

def path_to_list(path):
    """
    Convert a slash-separated path to a list.

    If the argument is already a list, it is returned as-is.

    >>> path_to_list('/')
    []
    >>> path_to_list('//one//')
    ['one']
    >>> path_to_list('one//three') # illegal
    ['one', '', 'three']
    >>> path_to_list('one/two/three')
    ['one', 'two', 'three']
    >>> path_to_list(path_to_list('one/two/three'))
    ['one', 'two', 'three']

    >>> numbers = path_to_list(u'/yksi/kaksi/kolme/nelj\xe4')
    >>> numbers[-1] == 'nelj\xc3\xa4'
    True

    >>> path_to_list('0x/0x2e/0x2e2e/0x2e2e2e')
    ['', '.', '..', '...']
    """
    if isinstance(path, list):
        return path

    path = path.strip('/')
    if not path:
        return []
    else:
        return map(hex_decode, path.split('/'))

def list_to_path(key):
    """
    Convert a list to a slash-separated path.

    >>> list_to_path(['one', 'two', 'three'])
    u'one/two/three'
    >>> list_to_path([])
    u''

    >>> list_to_path(['.', '..', '...'])
    u'0x2e/0x2e2e/...'
    >>> list_to_path(['///'])
    u'0x2f2f2f'
    >>> list_to_path(['0x...', '\xff', '\x01'])
    u'0x30782e2e2e/0xff/0x01'
    >>> numbers = ['yksi', 'kaksi', 'kolme', 'nelj\xc3\xa4']
    >>> list_to_path(numbers) == u'yksi/kaksi/kolme/nelj\xe4'
    True
    """
    return u'/'.join(map(hex_encode_for_path, key))

def hex_encode_for_path(s):
    assert isinstance(s, str)
    if '/' in s or s in ('', '.', '..'):
        return hex_encode(s)
    else:
        return hex_encode_for_text(s)

control_chars = set(range(1, 0x20) + range(0x7f, 0xa0))
map_bad_chars_to_nulls = dict((c, 0) for c in control_chars)

def hex_encode_for_text(s):
    # Hex-encode s if it starts with "0x", is not valid UTF-8, or
    # includes unicode control characters.

    if s.startswith('0x'):
        return hex_encode(s)

    try:
        t = s.decode('utf-8')
    except UnicodeError:
        return hex_encode(s)

    if u'\x00' in t.translate(map_bad_chars_to_nulls):
        return hex_encode(s)
    else:
        return t

def hex_encode(s):
    return u'0x' + hexlify(s)

def hex_decode(t):
    if t.startswith('0x'):
        return unhexlify(t[2:])
    else:
        return encode_text(t)

def encode_text(t):
    """
    Encode the argument using UTF-8 if it is unicode.

    >>> encode_text(u'y\xf6') == 'y\xc3\xb6'
    True
    >>> encode_text('night')
    'night'
    """
    if isinstance(t, unicode):
        return t.encode('utf-8')
    else:
        return t

def decode_text(s, default=None):
    """
    Convert a UTF-8 string to unicode.

    An empty string is returned if the argument is not valid UTF-8.
    For convenience, the specified default is returned if the argument
    is None.

    >>> decode_text('y\xc3\xb6') == u'y\xf6'
    True
    >>> decode_text('\xff')
    u''

    >>> decode_text(None)
    >>> decode_text(None, default=u'')
    u''
    """
    if s is None:
        return default

    try:
        return s.decode('utf-8')
    except UnicodeError:
        return u''

if __name__ == '__main__':
    import doctest
    doctest.testmod()
